package UI;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;

import Util.MiscUtil;

import Data.Character;
import Data.QueryException;
import Data.DbInterface;
import Logic.Settings;

/**
 * A shell which gives the user the search interface of the database so they
 * can build their armies for the game.
 */
public class ArmyBuilderShell extends BaseShell
{
	private Button startButton;
	
	private Button freeRadio;
	private Text freeText;
	
	private Button specificRadio;
	private Combo nameText;
	private Combo rankText;
	private Combo powerText;
	private Combo specText;
	private Combo weightText;
	private Combo heightText;
	
	private Label inSearchLabel;
	private Button searchButton;
	
	private Table resultsTable;
	
	private Composite creatureComposite;
	private Button[][] creatureButtons;
	
	private List<Character> searchResults;
	private Character[][] teams;
	
	private PlayerArmiesSelectedCallback callback;
	
	public ArmyBuilderShell(PlayerArmiesSelectedCallback callback)
	{
		super();
		
		this.callback = callback;
		
		searchResults = new ArrayList<Character>();
		teams = new Character[2][Settings.getArmySize()];
		
		setText("Build Your Army!");
		
		////////////// Start button //////////////////////
		
		startButton = new Button(this, SWT.PUSH);
		startButton.setText("Start Game");
		
		FormData formData = new FormData();
		formData.top = new FormAttachment(0, 0);
		formData.left = new FormAttachment(50, -100);
		formData.width = 200;
		formData.height = 70;
		
		startButton.setLayoutData(formData);
		
		startButton.addSelectionListener(new SelectionAdapter()
		{
			@Override
			public void widgetSelected(SelectionEvent e)
			{
				onStartClicked();
			}
		});
		
		//////////////// Free text search ////////////////////
		
		RadioListener radioListener = new RadioListener();
		
		freeRadio = new Button(this, SWT.RADIO);
		freeRadio.addSelectionListener(radioListener);
		freeRadio.setText("Free text:");
		freeRadio.setSelection(true);
		
		formData = new FormData();
		formData.left = new FormAttachment(0, 0);
		formData.top = new FormAttachment(0, 100);
		formData.width = 100;
		formData.height = 20;
		
		freeRadio.setLayoutData(formData);
		
		freeText = new Text(this, SWT.SINGLE | SWT.BORDER);
		
		formData = new FormData();
		formData.left = new FormAttachment(freeRadio, 20, SWT.LEAD);
		formData.top = new FormAttachment(freeRadio, 10, SWT.BOTTOM);
		formData.right = new FormAttachment(50, -15);
		formData.height = 20;
		
		freeText.setLayoutData(formData);
		
		//////////////// Specific search /////////////////////////
		
		specificRadio = new Button(this, SWT.RADIO);
		specificRadio.addSelectionListener(radioListener);
		specificRadio.setText("Specific:");
		
		formData = new FormData();
		formData.left = new FormAttachment(50, 15);
		formData.top = new FormAttachment(freeRadio, 0, SWT.TOP);
		formData.width = 100;
		formData.height = 20;
		
		specificRadio.setLayoutData(formData);
		
		nameText = addSearchFieldText(null, "Name", "characters");
		rankText = addSearchFieldText(nameText, "Rank", "rank");
		powerText = addSearchFieldText(rankText, "Power", "power");
		specText = addSearchFieldText(powerText, "Species", "species");
		
		weightText = addSearchFieldText(specText, "Weight", null);
		for (int i = 0; i <= 300; i+=10)
			weightText.add(String.valueOf(i));
		
		heightText = addSearchFieldText(weightText, "Height", null);
		DecimalFormat formater = new DecimalFormat("#.#");
		for (float h = 0; h <= 2.1f; h+=0.2f)
			heightText.add(formater.format(h));

		////////////////////// Search button /////////////////////////
		
		searchButton = new Button(this, SWT.PUSH);
		searchButton.setText("Search");
		
		formData = new FormData();
		formData.left = new FormAttachment(100, -100);
		formData.top = new FormAttachment(heightText, 20, SWT.BOTTOM);
		formData.width = 100;
		formData.height = 30;
		
		searchButton.setLayoutData(formData);
		
		setDefaultButton(searchButton);
		
		searchButton.addSelectionListener(new SelectionAdapter()
		{
			@Override
			public void widgetSelected(SelectionEvent e)
			{
				onSearchClicked();
			}
		});
		
		inSearchLabel = new Label(this, SWT.RIGHT);
		inSearchLabel.setText("Please wait, searching...");
		inSearchLabel.setVisible(false);
		
		formData = new FormData();
		formData.right = new FormAttachment(searchButton, -20, SWT.LEAD);
		formData.top = new FormAttachment(searchButton, 10, SWT.TOP);
		formData.width = 200;
		formData.height = 20;
		
		inSearchLabel.setLayoutData(formData);
		
		///////////////////// Creatures //////////////////
		
		creatureComposite = new Composite(this, SWT.NONE);
		creatureComposite.setLayout(new FillLayout());
		
		formData = new FormData();
		formData.left = new FormAttachment(0, 0);
		formData.right = new FormAttachment(100, 0);
		formData.bottom = new FormAttachment(100, 0);
		formData.height = 100;
		
		creatureComposite.setLayoutData(formData);
		
		creatureButtons = new Button[2][];
		creatureButtons[0] = new Button[5];
		creatureButtons[1] = new Button[5];
		
		for (int p = 0; p < 2; p++)
		{
			for (int i = 0; i < Settings.getArmySize(); i++)
			{
				creatureButtons[p][i] = new Button(creatureComposite, SWT.FLAT);
				creatureButtons[p][i].addSelectionListener(new CreatureButtonListener(p, i));
			}
			
			if (p == 0)
			{
				new Composite(creatureComposite, SWT.NONE);
			}
		}
		
		//////////////////////// Results //////////////////////////////
		
		Label resultsLabel = new Label(this, SWT.LEFT);
		resultsLabel.setText("Results:");
		
		formData = new FormData();
		formData.left = new FormAttachment(0, 0);
		formData.top = new FormAttachment(searchButton, 30, SWT.BOTTOM);
		formData.width = 100;
		formData.height = 20;
		
		resultsLabel.setLayoutData(formData);
		
		resultsTable = new Table(this, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER);
		resultsTable.setLinesVisible(true);
		resultsTable.setHeaderVisible(true);
		
		formData = new FormData();
		formData.left = new FormAttachment(0, 0);
		formData.top = new FormAttachment(resultsLabel, 10, SWT.BOTTOM);
		formData.right = new FormAttachment(100, 0);
		formData.bottom = new FormAttachment(creatureComposite, -30, SWT.TOP);
		
		resultsTable.setLayoutData(formData);
		
		String[] tableTitles = {
				"ID", "Name", "Power", "Species", "Universe", "Rank",
				"Weight", "Height", "Gender", "Medical Condition", "Occupation" };
		
		int[] tableWidths = {
				100, 200, 200, 200, 200, 200,
				100, 100, 100, 200, 200 };
		
		for (int i = 0; i < tableTitles.length; i++)
		{
			TableColumn column = new TableColumn(resultsTable, SWT.NONE);
			
			column.setText(tableTitles[i]);
			column.setWidth(tableWidths[i]);
		}
		
		resultsTable.addMouseListener(new MouseAdapter()
		{
			@Override
			public void mouseDoubleClick(MouseEvent e)
			{
				onTableItemDoubleClicked();
			}
		});
	}
	
	/**
	 * Add a search field combo box and label.
	 * 
	 * @param relativeTo The control relative to which to add the the label/combo box
	 * @param labelStr The label text to display
	 * @param autoCompleteAttrib The attribute type this combo should display in its autocomplete
	 * @return
	 */
	private Combo addSearchFieldText(Control relativeTo, String labelStr, String autoCompleteAttrib)
	{
		Label label = new Label(this, SWT.LEFT);
		label.setText(labelStr + ":");
		
		FormData formData = new FormData();
		
		if (relativeTo == null)
		{
			formData.left = new FormAttachment(specificRadio, 40, SWT.LEAD);
			formData.top = new FormAttachment(specificRadio, 15, SWT.BOTTOM);
			formData.width = 60;
			formData.height = 20;
		}
		else
		{
			formData.left = new FormAttachment(specificRadio, 40, SWT.LEAD);
			formData.top = new FormAttachment(relativeTo, 10, SWT.BOTTOM);
			formData.width = 60;
			formData.height = 20;
		}
		
		label.setLayoutData(formData);
		
		final Combo result = new Combo(this, SWT.DROP_DOWN | SWT.BORDER);
		result.setEnabled(false);
		
		boolean autoComplete = (autoCompleteAttrib != null);
		
		if (autoComplete)
		{
			result.setData(autoCompleteAttrib);
			
			result.addKeyListener(new KeyAdapter()
			{
				 public void keyReleased(KeyEvent ke) 
		         {
					 String text = result.getText();
					 
					 if ((result != nameText) || (text.length() >= 3))
					 {
						 setAutoCompletion(result, text);
					 }
					 else
					 {
						if (result.getItemCount() > 0)
							result.remove(0, Math.max(0, result.getItemCount() - 1));
						
						 result.setListVisible(false);
					 }
		         }
			});
		}
		
        formData = new FormData();
		formData.left = new FormAttachment(label, 0, SWT.TRAIL);
		formData.top = new FormAttachment(label, -5, SWT.TOP);
		formData.right = new FormAttachment(100, 0);
		formData.height = 20;
		
		result.setLayoutData(formData);
		
		return result;
	}
	
	/**
	 * Queries the database for autocompletion of the given field string.
	 * The autocomplete results are displayed in the drop-down of the combo box.
	 * 
	 * @param combo The combo for which to set the autocompletion
	 * @param query The query text to autocomplete
	 */
	private void setAutoCompletion(final Combo combo, final String query)
	{
		// manual remove range to prevent removing of selection text
		if (combo.getItemCount() > 0)
			combo.remove(0, Math.max(0, combo.getItemCount() - 1));
		
		final String attrib = (String)combo.getData();
		
		new Thread(new Runnable()
		{
			@Override
			public void run()
			{
				List<String> results = null;
				
				try
				{
					results = DbInterface.getGeneralAttribute(attrib, query);
					
					onAtributeSearchComplete(combo, results);
				}
				catch (QueryException e)
				{
					onSearchFailed();
				}
			}
		}).start();
	}
	
	private class RadioListener extends SelectionAdapter
	{
		@Override
		public void widgetSelected(SelectionEvent e)
		{
			boolean freeSelected = freeRadio.getSelection();
			boolean specificSelected = specificRadio.getSelection();
			
			freeText.setEnabled(freeSelected);
			
			nameText.setEnabled(specificSelected);
			rankText.setEnabled(specificSelected);
			powerText.setEnabled(specificSelected);
			specText.setEnabled(specificSelected);
			weightText.setEnabled(specificSelected);
			heightText.setEnabled(specificSelected);
		}
	}
	
	/**
	 * Called when the search button is clicked.
	 * If the free text option is selected, a free text search is performed.
	 * If the specific option is selected, a search for the specific attributes is performed.
	 * Once the results are in, the table view is populated.
	 */
	private void onSearchClicked()
	{
		setControlsInSearchMode(true);
		
		if (freeRadio.getSelection())
		{
			final String searchTerm = freeText.getText();
			
			new Thread(new Runnable()
			{
				@Override
				public void run()
				{
					List<Integer> characters = null;
					
					try
					{
						characters = DbInterface.search(searchTerm);
						
						onSearchComplete(characters);
					}
					catch (QueryException e)
					{
						onSearchFailed();
					}
				}
			}).start();
		}
		else
		{
			String name = nameText.getText();
			String rank = rankText.getText();
			String power = powerText.getText();
			String species = specText.getText();
			
			if (name.length() == 0) name = null;
			if (rank.length() == 0) rank = null;
			if (power.length() == 0) power = null;
			if (species.length() == 0) species = null;
			
			double weight = 0.0;
			double height = 0.0;
			
			double minHeight = 0.0;
			double maxHeight = 0.0;
			double minWeight = 0.0;
			double maxWeight = 0.0;
			
			try
			{
				if (weightText.getText().length() > 0)
				{
					weight = Double.parseDouble(weightText.getText());
					
					minWeight = weight - 10;
					maxWeight = weight + 10;
				}
				
				if (heightText.getText().length() > 0)
				{
					height = Double.parseDouble(heightText.getText());
					
					minHeight = height - 0.05;
					maxHeight = height + 0.05;
				}
			}
			catch (NumberFormatException e)
			{
				MessageBox mb = new MessageBox(this);
				
				mb.setText("Error");
				mb.setMessage("Unable to parse weight/height");
				mb.open();
				
				return;
			}
			
			final String fName = name;
			final String fPower = power;
			final String fSpecies = species;
			final String fRank = rank;
			final double fMinHeight = minHeight;
			final double fMaxHeight = maxHeight;
			final double fMinWeight = minWeight;
			final double fMaxWeight = maxWeight;
			
			new Thread(new Runnable()
			{
				@Override
				public void run()
				{
					List<Integer> characters = null;
					
					try
					{
						characters = DbInterface.search(fName, null, fPower, fSpecies, null,
								fRank, null, null, fMinHeight, fMaxHeight, fMinWeight, fMaxWeight);
						
						onSearchComplete(characters);
					}
					catch (QueryException e)
					{
						onSearchFailed();
					}
				}
			}).start();
		}
	}
	
	/**
	 * Updates the search results with the characters returned from the search.
	 * The data structure is updated and the table view is populated.
	 * 
	 * @param characters The characters returned from the search.
	 */
	private void onSearchComplete(List<Integer> characters)
	{
		synchronized (searchResults)
		{
			searchResults.clear();
			
			if (characters.size() > 0)
			{
				try
				{
					searchResults.addAll(DbInterface.getCharactersFromIDs(characters));
				}
				catch (QueryException e)
				{
					onSearchFailed();
				}
			}
		}
		
		getDisplay().asyncExec(new Runnable()
		{
			@Override
			public void run()
			{
				setControlsInSearchMode(false);
				
				populateTable();
			}
		});
	}
	
	/**
	 * Called when the query of the attribute autocompletion is returned.
	 * The method sets the dropdown data appropriately to show the autocomplete options.
	 *  
	 * @param combo The combo in which to show the results.
	 * @param results The results of the attribute search
	 */
	private void onAtributeSearchComplete(final Combo combo, final List<String> results)
	{	
		getDisplay().asyncExec(new Runnable()
		{
			@Override
			public void run()
			{
				combo.setListVisible(true);
				
				for (String result : results)
					combo.add(result);			
			}
		});
	}
	
	/**
	 * Called when the various types of searches fail.
	 * Displays an error message.
	 */
	private void onSearchFailed()
	{
		getDisplay().asyncExec(new Runnable()
		{
			@Override
			public void run()
			{
				setControlsInSearchMode(false);
				
				MessageBox mb = new MessageBox(ArmyBuilderShell.this);
				
				mb.setText("Error");
				mb.setMessage("Unable to complete the requested search query.");
				mb.open();
			}
		});
	}
	
	/**
	 * Populates the table view with the search results.
	 */
	private void populateTable()
	{
		resultsTable.removeAll();
		
		synchronized (searchResults)
		{
			for (Character ch : searchResults)
			{
				TableItem tableItem = new TableItem(resultsTable, SWT.NONE);
				
				tableItem.setText(0, Integer.toString(ch.getId()));
				tableItem.setText(1, ch.getName());
				tableItem.setText(2, MiscUtil.aggregateToString(ch.getPower()));
				tableItem.setText(3, MiscUtil.aggregateToString(ch.getSpecies()));
				tableItem.setText(4, MiscUtil.aggregateToString(ch.getUniverse()));
				tableItem.setText(5, MiscUtil.aggregateToString(ch.getRank()));
				tableItem.setText(6, Float.toString(ch.getWeight()));
				tableItem.setText(7, Float.toString(ch.getHeight()));
				tableItem.setText(8, ch.getGender());
				tableItem.setText(9, MiscUtil.aggregateToString(ch.getMedicalCondition()));
				tableItem.setText(10, MiscUtil.aggregateToString(ch.getOccupation()));
			}
		}
	}
	
	/**
	 * Returns the character data currently selected in the table view.
	 * 
	 * @return The character data
	 */
	private Character getSelectedCharacter()
	{
		int index = resultsTable.getSelectionIndex();
		
		synchronized (searchResults)
		{
			if ((index < 0) || (index >= searchResults.size()))
				return null;
			
			return searchResults.get(index);
		}
	}
	
	/**
	 * When a character in the table view is double-clicked, the character info shell is
	 * opened to show the character information and allow deletion.
	 */
	private void onTableItemDoubleClicked()
	{
		Character ch = getSelectedCharacter();
		
		if (ch == null)
			return;
		
		CharacterInfoShell charInfoShell = new CharacterInfoShell(ch);
		SWTUtil.openShell(charInfoShell);
		
		if (charInfoShell.isCharacterDeleted())
			onSearchClicked(); //refresh search after deleting a character
	}
	
	/**
	 * Class for listening for army button clicks.
	 */
	private class CreatureButtonListener extends SelectionAdapter
	{
		private int playerIndex;
		private int creatureIndex;
		
		public CreatureButtonListener(int playerIndex, int creatureIndex)
		{
			this.playerIndex = playerIndex;
			this.creatureIndex = creatureIndex;
		}
		
		/**
		 * When a button is clicked, add the currently selected character to the
		 * player's army. If this character is already in this player's army, an error
		 * message is displayed.
		 */
		@Override
		public void widgetSelected(SelectionEvent e)
		{
			Character ch = getSelectedCharacter();
			
			if (ch == null)
			{
				MessageBox mb = new MessageBox(ArmyBuilderShell.this);
				
				mb.setText("Error");
				mb.setMessage("Please select a character from the search results first");
				mb.open();
				
				return;
			}
			
			for (int i = 0; i < teams[playerIndex].length; i++)
			{
				if (i != creatureIndex && 
					teams[playerIndex][i] != null && 
					teams[playerIndex][i].getId() == ch.getId())
				{
					MessageBox mb = new MessageBox(ArmyBuilderShell.this);
					
					mb.setText("Error");
					mb.setMessage("Character already selected for this army");
					mb.open();
					
					return;
				}
			}
			
			creatureButtons[playerIndex][creatureIndex].setText(ch.getName());
			teams[playerIndex][creatureIndex] = ch;
		}
	}
	
	/**
	 * Toggles the window controls' search mode.
	 * When in search mode all the controls are disabled.
	 * 
	 * @param inSearch Whether or not the window is currently during a search.
	 */
	private void setControlsInSearchMode(boolean inSearch)
	{
		boolean inIdle = !inSearch;
		
		inSearchLabel.setVisible(inSearch);
		
		startButton.setEnabled(inIdle);
		searchButton.setEnabled(inIdle);
		resultsTable.setEnabled(inIdle);
		
		for (int i = 0; i < creatureButtons.length; i++)
			for (int j = 0; j < creatureButtons[0].length; j++)
				creatureButtons[i][j].setEnabled(inIdle);
	}
	
	/**
	 * Called when the start button is clicked.
	 * Once clicked, the callback is called (back) and the game begins.
	 */
	private void onStartClicked()
	{
		for (int i = 0; i < teams.length; i++)
		{
			for (int j = 0; j < teams[0].length; j++)
			{
				if (teams[i][j] == null)
				{
					MessageBox mb = new MessageBox(this);
					
					mb.setText("Error");
					mb.setMessage("Please complete both armies.");
					mb.open();
					
					return;
				}
			}
		}
		
		callback.onPlayerArmiesSelected(this, teams[0], teams[1]);
	}
}
